<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>什么是测试驱动开发？</title>
    <meta content="1200" property="og:image:width"/>
    <meta content="630" property="og:image:height"/>
    <link rel="icon" href="../../img/icon.png">
    <link as="font" crossorigin="" href="../../Agrandir-Heavy.2fd076131b76.woff2" rel="preload"
          type="font/woff2"/>
    <link as="font" crossorigin="" href="../../Agrandir-Bold.5adcade67872.woff2" rel="preload"
          type="font/woff2"/>
    <link as="font" crossorigin="" href="../../source-sans-pro-v14-latin-regular.899c8f78ce65.woff2"
          rel="preload" type="font/woff2"/>
    <link as="font" crossorigin="" href="../../Agrandir-Regular.11a4ceb99823.woff2" rel="preload"
          type="font/woff2"/>
    <link as="font" crossorigin="" href="../../source-sans-pro-v14-latin-600.c85615b29630.woff2" rel="preload"
          type="font/woff2"/>
    <link href="../../common.0016184b0568.css" rel="stylesheet" type="text/css"/>
    <link href="../../home.a12d472bcf09.css" rel="stylesheet" type="text/css"/>
    <link href="../content-page.css" rel="stylesheet" type="text/css"/>
</head>
<header class="site-header">
    <br/>
    <h1 style="margin: 0 auto;text-align: left;margin: 0 50px;color: white;font-size: 3.5rem">
        <img src="../../svg/icon-white.svg" height="80" width="80"/>&nbsp;&nbsp;Neil's wiki
        <button class="subscribe" onclick="location.href='../subscribe/index.html'">Subscribe</button>
    </h1>
    <br/>
</header>
<body class="content-body">
<div class="content-body-class">
    <h1 class="hosting-pricing-plan-block__header">什么是测试驱动开发？</h1>
    <div class="content-infos">
        <div class="content-words">
            测试驱动开发是一种软件开发实践，源于1999年Kent Beck《Extreme Programming Explained》一书中的测试先行这一概念。Kent Beck在2003年再次提到 –
            TDD鼓励简单的设计并激发信心。经过后期的发展，TDD已经成长为一门独立的软件开发技术，其名气甚至盖过了极限编程。
        </div>
        <div class="content-image">
            <img src="../../img/tdd/what-is-tdd/xp-circle.png"/>
        </div>
        <div class="content-words">
            TDD采用了一种以终为始的思维方式，它依赖于非常短的开发周期的重复：先将需求转换为非常具体的测试用例，然后改进代码，从而让测试通过。实际编码过程体现为：在开发业务功能代码之前，先编写测试代码。测试代码确定了我们要验收什么以及如何验收，然后再去编写功能代码，当测试通过时，代表功能完成。
        </div>
        <div class="content-words">
            在实践过程中，人们常常将TDD区分为广义TDD和狭义TDD。广义的TDD是ATDD（Acceptance Test Driven Development），狭义的TDD，常常说的是UTDD（Unit Test
            Driven Development）。 本文所说的TDD更多指狭义上的TDD。
        </div>
        <div class="content-words">
            在实践TDD的过程中，我们对TDD形成了三层理解：
        </div>
        <div class="content-unlist">
            • Task-Driven Development，任务驱动开发。
        </div>
        <div class="content-unlist">
            • Test-Driven Development，测试驱动开发。
        </div>
        <div class="content-unlist">
            • Test-Driven Design，测试驱动设计。
        </div>
        <div class="content-h1">
            Task-Driven Development
        </div>
        <div class="content-words">
            任务拆解是TDD的一个关键的步骤。在开始编写进入代码环节之前，需要对业务需求做拆分，这个过程我们把它称之为Tasking。比如，一个用户登录的业务需求，我们大体可以拆分成如下几个Task：
        </div>
        <div class="content-list">
            1. 假定用户名不存在时，当用户登录，则登录失败
        </div>
        <div class="content-list">
            2. 假定用户名正确且密码错误时，当用户登录，则登录失败
        </div>
        <div class="content-list">
            3. 假定用户名和密码都正确时，当用户登录，登录成功
        </div>
        <div class="content-words">
            通过上面的三个Tasking，可以总结出一个模式：Given When Then：
        </div>
        <div class="content-list">
            1. Given 用户名不存在，When 用户登录， Then 登录失败
        </div>
        <div class="content-list">
            2. Given 用户名正确且密码错误，When 用户登录， Then 登录失败
        </div>
        <div class="content-list">
            3. Given 用户名和密码都正确，When 用户登录， Then 登录成功
        </div>
        <div class="content-words">
            Given When Then借鉴了BBD（Behavior Driven
            Development）里面的模式，它更加关注用户如何使用系统，即系统所提供的功能，从理解上更偏向于业务语言。对于一些技术人员，在初学阶段，需要多去练习，体会不同点，可以选业务人员视角索要一些反馈。
        </div>
        <div class="content-words">
            在TDD中，我将之称为Tasking三步曲：
        </div>
        <div class="content-unlist">
            • Given，代表特定的业务场景
        </div>
        <div class="content-unlist">
            • When，代表用户发生的行为
        </div>
        <div class="content-unlist">
            • Then，代表行为产生的结果
        </div>
        <div class="content-h1">
            Test-Driven Development
        </div>
        <div class="content-words">
            上一步Tasking拆分好的一系列Task，经过沟通澄清之后，就可以将这些Task翻译成测试。在翻译的过程中需要注意的一个核心点是 – 保持业务概念的统一（统一语言的应用）。如何理解这一点呢，来个看一个Task：
        </div>
        <div class="content-words">
            <span class="content-mention">
                Given 一个有空位的停车场，When 停车，Then 停车成功，返回一张票据
            </span>
        </div>
        <div class="content-words">
            翻译成测试代码后：
        </div>
        <div class="markdown">
            @Test<br/>
            void should_return_ticket_when_parking_given_a_parking_lot_has_available_space(){<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspParkingLot parkinglot = new ParkingLot(1);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspString car = "布加迪"<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspString ticket = parkinglot.park(car);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspassertNotNull(ticket);<br/>
            }
        </div>
        <div class="content-words">
            在上述测试代码中，car和ticket都是用了字符串来表示（基本类型偏执），车和票据这两个业务概念没有体现出来，建议是使用
            <span class="content-mention">
                Car
            </span>
            和
            <span class="content-mention">
                Ticket
            </span>
            来代替这两个业务概念。
        </div>
        <div class="content-words">
            极限的TDD，会将以终为始的思维运用到极致。它提倡在写测试用例的时候，先写断言，然后一步一步往前倒逼驱动出来测试代码，比如上述的例子，经历的过程如下：
        </div>
        <div class="markdown">
            {<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp// 1. 我最终要验收的是停车票，代表停车成功<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspassertNotNull(ticket);<br/>
            }<br/>
            <br/>
            {<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp// 2. 停车票是停车后返回的<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspTicker ticket = parkinglot.park(car);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspassertNotNull(ticket)<br/>
            }<br/>
            <br/>
            {<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp// 3. 停的车是我新开进来的车<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspCar car = new Car();<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspTicker ticket = parkinglot.park(car);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspassertNotNull(ticket)<br/>
            }<br/>
            <br/>
            @Test<br/>
            void should_return_ticket_when_parking_given_a_parking_lot_has_available_space() {<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp// 4. 停进了有空位的停车场<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspParkingLot parkinglot = new ParkingLot(1);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspCar car = new Car();<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspTicker ticket = parkinglot.park(car);<br/>
            &nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbsp&nbspassertNotNull(ticket)<br/>
            }
        </div>
        <div class="content-words">
            这种方式在刚开始是很难做到的，但是通过多次可以练习，形成这种行为习惯，内化成思维习惯，也是益处多多，非常建议你去刻意练习。
        </div>
        <div class="content-h1">
            Test-Driven Design
        </div>
        <div class="content-words">
            <span class="content-mention">
                TDD并不会驱动出好的设计，TDD只会给你及时的反馈什么可能是糟糕的设计 – Kent Beck
            </span>
        </div>
        <div class="content-words">
            在《百问TDD》中有一个问题能够很好地解释这一点，它是
            <a href="https://xpbootcamp.cn/tdd/questions/001">测试如何驱动设计？</a>
        </div>
        <div class="content-h1">
            为什么要 TDD？
        </div>
        <div class="content-h2">
            及时澄清需求
        </div>
        <div class="content-words">
            Task Driven
            Development，我们拿到业务需求后会做Tasking，通过Tasking梳理业务需求，将一个大问题进行拆解后，从而能够深入理解业务细节。把一个个业务场景拆分罗列出来后，我们基本上对业务了如指掌，同时你也可以拿着拆分结果，去找业务人员沟通确认，以及澄清一些疑惑点。这个过程恰好体现了敏捷的核心精髓
            – 及时反馈。
        </div>
        <div class="content-words">
            通过前期的反馈，能够避免因为业务需求理解偏差付出的无用功。
        </div>
        <div class="content-h2">
            促进新人培养
        </div>
        <div class="content-words">
            如果团队来了几个新人，你需要带他们快速进入正常的业务开发，而迫于有限的精力，你不能做到同时跟几个人Pair。此时，教会他们Tasking是非常有帮助。你能够从新人的Tasking结果中能够快速确认他们对业务需求的理解是否正确，提前给出指导反馈，控制好方向。
        </div>
        <div class="content-words">
            另外，培养新人以终为始的这种思维习惯也是对他们的职业生涯有益的。
        </div>
        <div class="content-h2">
            分离关注点
        </div>
        <div class="content-words">
            TDD的方式，写测试代码时，我们只关注功能验收，而且只关注特定的一个Task。写功能代码时，只用关注如何完成本次验收，即让当前的测试用例通过。测试通过后，你只用关注代码设计上，思考要不要将代码从刚刚可用的状态改良成简洁可用的状态。这个过程很好分离了我们的关注点，降低思维的负担，帮助专注聚焦，有效地提升开发效率。
        </div>
        <div class="content-h2">
            构建保护网
        </div>
        <div class="content-words">
            TDD的一个必然结果产生是高价值、高覆盖的单元测试，为功能代码提供了一个保护网。这样一来，我们可以充满信心地进行代码重构，去改善我们的代码设计。
        </div>
        <div class="content-words">
            另外，相信你在项目上遇到过因为代码集成造成的功能破坏，这种情况多半是代码没有足够测试覆盖，集成时被破坏后没法及时发现。反馈延迟，必然增加维护成本，所以通过构建测试保护网，将大大缓解这种痛点。
        </div>
        <div class="content-h2">
            减免调试
        </div>
        <div class="content-words">
            可能你会发现，采用TDD后，代码量会增加，貌似开发效率就会降低了。试想一下，如果没有单元测试，你就可能要手工测试（如果你还是一位负责人的专业程序员）。那么，手工测试时，在准备数据、启动应用、界面操作等事项上会花费你很多时间，而且大多数工作还需要重复去做，并且获得反馈周期拉长。
        </div>
        <div class="content-words">
            有了单元测试，你可以省去很多重复的手工操作，每次运行测试能够快速获得反馈，减免了繁琐的调试工作。
        </div>
        <div class="content-h1">
            怎么做TDD？
        </div>
        <div class="content-h2">
            TDD流程
        </div>
        <div class="content-words">
            我们常说，作为一个TDDer，要时刻切换三顶帽子（红帽子、绿帽子~ ~、蓝帽子），其实是说的是TDD的过程：红（编写测试）、绿（编写实现）、蓝（重构）
        </div>
        <div class="content-image">
            <img src="../../img/tdd/what-is-tdd/tdd-circle.png"/>
        </div>
        <div class="content-words">
            对于初学者，在做TDD的时候，有一个更加详细的流程供我们参考：
        </div>
        <div class="content-image">
            <img src="../../img/tdd/what-is-tdd/tdd-circle.png"/>
        </div>
        <div class="content-words">
            我才新写了一个测试用例，明知道它会运行失败（另外还有编译错误），还要运行吗？
        </div>
        <div class="content-words">
            我建议你运行一下。
        </div>
        <div class="content-words">
            首先，作为新手，这是一种节奏感和习惯的养成，一些行为习惯会潜移默化地影响你思考方式。其次，测试失败有很多种情况，你需要通过运行测试来获得反馈，这个失败是不是你期望的失败，比如：
        </div>
        <div class="content-unlist">
            • 如期失败：是不是接口还不存在，编译错误？那我需要去实现我的接口
        </div>
        <div class="content-unlist">
            • 如期失败：是不是新功能逻辑还没有实现，断言失败？那我接下就需要去实现新功能
        </div>
        <div class="content-unlist">
            • 非如期失败：是不是不小心手误改了测试代码，导致其他的也失败了？那我需要当心了
        </div>
        <div class="content-unlist">
            • 非如期成功，测试通过了，我的测试是不是写的有问题？那我需要当心了
        </div>
        <div class="content-h1">
            三条童子军规
        </div>
        <div class="content-words">
            在美国童子军中，流传一条这样的军规：让
            <span class="content-mention">营地比你来时更干净
            </span>
            。Bob大叔在他的《代码整洁之道》中也引用这个词，提倡持续改进 – 作为一个专业高素养的程序员，要尽全力让项目代码随着时间流逝而越变越好。那么借用这个说法，在TDD中，我们也提倡三条童子军规：
        </div>
        <div class="content-unlist">
            • 没有测试之前不要写任何功能代码
        </div>
        <div class="content-unlist">
            • 一次只写一个刚好失败的测试，作为新加功能的描述
        </div>
        <div class="content-unlist">
            • 不写任何多余的产品代码，让它刚好能让失败的测试通过就好
        </div>
        <div class="content-words">
            如果违反了这几条童子军规，会怎么样呢？
        </div>
        <div class="content-unlist">
            • 违反第一条，如果先编写了产品代码，那这段代码实现了什么业务需求呢？你又怎么确保它真的实现了呢？手工测试，太费时间。后补测试，交付压力这么大，还是先写新功能吧。
        </div>
        <div class="content-unlist">
            • 违反第二条，如果写了多个测试，测试长时间没法通过，会增加开发者的心里压力。实际上，伴随着知识的积累，业务细节的深入，Task会发生变更，测试会随之修改，那之前写的测试就成了累赘。
        </div>
        <div class="content-unlist">
            • 违反第三条，产品代码实现了超出当前测试的功能，这部分代码就没有测试的保护，又回到第一条了。另外，多些的代码可能是不存在的需求，增加了没必要的复杂性。
        </div>
        <div class="content-words">
            据我观察，别说TDD的初学者，就是有一定经验的TDDer，也很难时刻做到一次只编写刚好让测试通过的功能代码。我还见过有些人一上来写上10来个测试用例。那么在实践练习的过程中，在开始守（守、破、离）的阶段，可以时刻用这三条军规提醒自己。
        </div>
        <div class="content-h1">
            TDD相关书籍
        </div>
        <div class="content-unlist">
            《编写可读代码的艺术》
        </div>
        <div class="content-unlist">
            《代码整洁之道》
        </div>
        <div class="content-unlist">
            <a href="https://github.com/VWWL/library/blob/main/计算机/代码设计/测试驱动开发.pdf">
                《测试驱动开发》
            </a>
        </div>
        <div class="content-unlist">
            <a href="https://github.com/VWWL/library/blob/main/计算机/代码设计/有效的单元测试.pdf">
                《有效的单元测试》
            </a>
        </div>
        <div class="content-unlist">
            《实例化需求》
        </div>
        <div class="content-unlist">
            <a href="https://github.com/VWWL/library/blob/main/计算机/代码设计/重构_改善既有代码的设计.pdf">
                《重构：改善既有代码的设计》
            </a>
        </div>
        <div class="content-addition">
            文章转载自<a href="https://xpbootcamp.cn/tdd/questions/001">袁慎建</a>
        </div>
    </div>
</div>
</body>
</html>
<div class="back-to-home-page">
    <a class="call-to-action__link button" href="/">返回首页</a>
</div>
